/**
 * Audio codec types
 */
export enum AudioCodec {
    AAC = 10
}

/**
 * Video codec types
 */
export enum VideoCodec {
    AVC = 7 // H.264
}

/**
 * Frame types
 */
export enum FrameType {
    KEY_FRAME = 1,
    INTER_FRAME = 2
}

/**
 * AVC packet types
 */
export enum AVCPacketType {
    SEQUENCE_HEADER = 0,
    NALU = 1,
    END_OF_SEQUENCE = 2
}

/**
 * AAC packet types
 */
export enum AACPacketType {
    SEQUENCE_HEADER = 0,
    RAW = 1
}

/**
 * Audio configuration
 */
export interface AudioConfig {
    sampleRate: number;      // Sample rate (e.g., 44100, 48000)
    channels: number;        // Number of channels (1 = mono, 2 = stereo)
    sampleSize: number;      // Sample size in bits (8 or 16)
}

/**
 * FLV data callback
 */
export type FlvDataCallback = (data: Uint8Array) => void;

/**
 * FLV Muxer - Encapsulates H.264 video and AAC audio into FLV format
 */
export class FlvMuxer {
    private onFlvData: FlvDataCallback;
    private timestamp: number = 0;
    private videoTimestamp: number = 0;
    private audioTimestamp: number = 0;
    private hasVideoSequenceHeader: boolean = false;
    private hasAudioSequenceHeader: boolean = false;

    /**
     * Constructor
     * @param callback Callback function to output FLV data
     */
    constructor(callback: FlvDataCallback) {
        this.onFlvData = callback;
    }

    /**
     * Create FLV header
     * @param hasAudio Whether the stream contains audio
     * @param hasVideo Whether the stream contains video
     * @returns FLV header bytes
     */
    public createFlvHeader(hasAudio: boolean = true, hasVideo: boolean = true): Uint8Array {
        const header = new Uint8Array(13);
        let offset = 0;

        // FLV signature
        header[offset++] = 0x46; // 'F'
        header[offset++] = 0x4C; // 'L'
        header[offset++] = 0x56; // 'V'

        // Version
        header[offset++] = 0x01;

        // Flags (audio + video)
        let flags = 0;
        if (hasAudio) flags |= 0x04;
        if (hasVideo) flags |= 0x01;
        header[offset++] = flags;

        // Header size (always 9)
        header[offset++] = 0x00;
        header[offset++] = 0x00;
        header[offset++] = 0x00;
        header[offset++] = 0x09;

        // Previous tag size (always 0 for header)
        header[offset++] = 0x00;
        header[offset++] = 0x00;
        header[offset++] = 0x00;
        header[offset++] = 0x00;

        console.log("FLV header created, hasAudio:", hasAudio, "hasVideo:", hasVideo, "header length:", header.length);
        return header;
    }

    /**
     * Write audio data
     * @param aacData AAC audio data
     * @param config Audio configuration
     * @param isSequenceHeader Whether this is AAC sequence header
     * @param timestamp Timestamp in milliseconds
     */
    public writeAudio(
        aacData: Uint8Array,
        config: AudioConfig,
        isSequenceHeader: boolean = false,
        timestamp?: number
    ): void {
        if (timestamp !== undefined) {
            this.audioTimestamp = timestamp;
        }

        // Calculate sound format flags
        const soundFormat = AudioCodec.AAC << 4;
        const soundRate = this.getSoundRate(config.sampleRate);
        const soundSize = config.sampleSize === 16 ? 1 : 0;
        const soundType = config.channels === 2 ? 1 : 0;
        const soundFlags = soundFormat | (soundRate << 2) | (soundSize << 1) | soundType;

        // AAC packet type
        const aacPacketType = isSequenceHeader ? AACPacketType.SEQUENCE_HEADER : AACPacketType.RAW;

        // Tag data size = 1 (sound flags) + 1 (AAC packet type) + AAC data size
        const dataSize = 1 + 1 + aacData.length;

        // Create FLV tag
        const tag = new Uint8Array(11 + dataSize + 4);
        let offset = 0;

        // Tag type (8 = audio)
        tag[offset++] = 0x08;

        // Data size (24-bit big-endian)
        tag[offset++] = (dataSize >> 16) & 0xFF;
        tag[offset++] = (dataSize >> 8) & 0xFF;
        tag[offset++] = dataSize & 0xFF;

        // Timestamp (24-bit big-endian)
        tag[offset++] = (this.audioTimestamp >> 16) & 0xFF;
        tag[offset++] = (this.audioTimestamp >> 8) & 0xFF;
        tag[offset++] = this.audioTimestamp & 0xFF;

        // Timestamp extended (8-bit)
        tag[offset++] = (this.audioTimestamp >> 24) & 0xFF;

        // Stream ID (always 0)
        tag[offset++] = 0x00;
        tag[offset++] = 0x00;
        tag[offset++] = 0x00;

        // Sound flags
        tag[offset++] = soundFlags;

        // AAC packet type
        tag[offset++] = aacPacketType;

        // AAC data
        tag.set(aacData, offset);
        offset += aacData.length;

        // Previous tag size
        const tagSize = 11 + dataSize;
        tag[offset++] = (tagSize >> 24) & 0xFF;
        tag[offset++] = (tagSize >> 16) & 0xFF;
        tag[offset++] = (tagSize >> 8) & 0xFF;
        tag[offset++] = tagSize & 0xFF;

        // console.log("FLV audio tag created, timestamp:", this.audioTimestamp, "isSeqHeader:", isSequenceHeader, "tag length:", tag.length);
        this.onFlvData(tag);

        if (isSequenceHeader) {
            this.hasAudioSequenceHeader = true;
        }
    }

    /**
     * Write video data
     * @param avcData H.264 AVCC format data (with 4-byte length prefix)
     * @param isKeyFrame Whether this is a key frame
     * @param isSequenceHeader Whether this is AVC sequence header (SPS/PPS)
     * @param timestamp Timestamp in milliseconds
     * @param compositionTime Composition time offset in milliseconds
     */
    public writeVideo(
        avcData: Uint8Array,
        isKeyFrame: boolean = false,
        isSequenceHeader: boolean = false,
        timestamp?: number,
        compositionTime: number = 0
    ): void {
        if (timestamp !== undefined) {
            this.videoTimestamp = timestamp;
        }

        // Frame type and codec ID
        let frameType = isKeyFrame ? FrameType.KEY_FRAME : FrameType.INTER_FRAME;
        const codecId = VideoCodec.AVC;
        const videoFlags = (frameType << 4) | codecId;

        if (isSequenceHeader) {
            frameType = FrameType.KEY_FRAME;
        }
        // AVC packet type
        const avcPacketType = isSequenceHeader ? AVCPacketType.SEQUENCE_HEADER : AVCPacketType.NALU;

        // Tag data size = 1 (video flags) + 1 (AVC packet type) + 3 (composition time) + AVC data size
        const dataSize = 1 + 1 + 3 + avcData.length;

        // Create FLV tag
        const tag = new Uint8Array(11 + dataSize + 4);
        let offset = 0;

        // Tag type (9 = video)
        tag[offset++] = 0x09;

        // Data size (24-bit big-endian)
        tag[offset++] = (dataSize >> 16) & 0xFF;
        tag[offset++] = (dataSize >> 8) & 0xFF;
        tag[offset++] = dataSize & 0xFF;

        // Timestamp (24-bit big-endian)
        tag[offset++] = (this.videoTimestamp >> 16) & 0xFF;
        tag[offset++] = (this.videoTimestamp >> 8) & 0xFF;
        tag[offset++] = this.videoTimestamp & 0xFF;

        // Timestamp extended (8-bit)
        tag[offset++] = (this.videoTimestamp >> 24) & 0xFF;

        // Stream ID (always 0)
        tag[offset++] = 0x00;
        tag[offset++] = 0x00;
        tag[offset++] = 0x00;

        // Video flags
        tag[offset++] = videoFlags;

        // AVC packet type
        tag[offset++] = avcPacketType;

        // Composition time (24-bit big-endian, signed)
        tag[offset++] = (compositionTime >> 16) & 0xFF;
        tag[offset++] = (compositionTime >> 8) & 0xFF;
        tag[offset++] = compositionTime & 0xFF;

        // AVC data
        tag.set(avcData, offset);
        offset += avcData.length;

        // Previous tag size
        const tagSize = 11 + dataSize;
        tag[offset++] = (tagSize >> 24) & 0xFF;
        tag[offset++] = (tagSize >> 16) & 0xFF;
        tag[offset++] = (tagSize >> 8) & 0xFF;
        tag[offset++] = tagSize & 0xFF;

        // console.log("FLV video tag created, timestamp:", this.videoTimestamp, "isKeyFrame:", isKeyFrame, "isSeqHeader:", isSequenceHeader, "tag length:", tag.length);
        this.onFlvData(tag);

        if (isSequenceHeader) {
            this.hasVideoSequenceHeader = true;
        }
    }

    /**
     * Get sound rate code from sample rate
     * @param sampleRate Sample rate in Hz
     * @returns Sound rate code (0-3)
     */
    private getSoundRate(sampleRate: number): number {
        if (sampleRate === 44100) return 3;
        if (sampleRate === 22050) return 2;
        if (sampleRate === 11025) return 1;
        return 3; // 5512 Hz
    }

    /**
     * Reset timestamps
     */
    public reset(): void {
        this.timestamp = 0;
        this.videoTimestamp = 0;
        this.audioTimestamp = 0;
        this.hasVideoSequenceHeader = false;
        this.hasAudioSequenceHeader = false;
    }

    /**
     * Check if video sequence header has been sent
     */
    public hasVideoSequence(): boolean {
        return this.hasVideoSequenceHeader;
    }

    /**
     * Check if audio sequence header has been sent
     */
    public hasAudioSequence(): boolean {
        return this.hasAudioSequenceHeader;
    }
}